/*
 * Copyright (C) 2018 The Asiainfo-Ability Authors
 *
 *      Licensed under the Apache License, Version 2.0 (the "License");
 *      you may not use this file except in compliance with the License.
 *      You may obtain a copy of the License at
 *
 *          http://www.apache.org/licenses/LICENSE-2.0
 *
 *      Unless required by applicable law or agreed to in writing, software
 *      distributed under the License is distributed on an "AS IS" BASIS,
 *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *      See the License for the specific language governing permissions and
 *      limitations under the License.
 */

package org.asiainfo.ability.node.service;

import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.asiainfo.ability.base.code.InstanceState;
import org.asiainfo.ability.base.queue.OperEvent;
import org.asiainfo.ability.base.queue.support.RouteServerEvent;
import org.asiainfo.ability.base.utils.ExceptionUtils;
import org.asiainfo.ability.base.utils.StringUtils;
import org.asiainfo.ability.gateway.configuration.OperConfig;
import org.asiainfo.ability.gateway.utils.SerializationUtils;
import org.asiainfo.ability.node.code.OperationType;
import org.asiainfo.ability.node.utils.ShellUtils;
import org.asiainfo.ability.node.vo.NodeOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import javax.annotation.PostConstruct;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * @Author: visen
 * @Date: 2018/1/27
 * @Description:
 */
@Service
public class NodeService {
    public static final String WORKSPACE = System.getProperty("user.dir") + File.separator + "workspace"
            + File.separator;
    public static final String REPO = WORKSPACE + "repo" + File.separator;
    //public static final String INSTANCE = WORKSPACE + "instance";
    private static final Logger logger = LogManager.getLogger(NodeService.class);

    @Value("${eureka.client.serviceUrl.defaultZone}")
    private String eurekaZone;

    @Value("${security.user.name}")
    private String adminUser;
    @Value("${security.user.password}")
    private String adminPassword;


    private String activeProfile;

    @Autowired
    private RedisTemplate redisTemplate;


    public void publishEvent(OperEvent event) {
        try {
            redisTemplate.execute(new RedisCallback<Object>() {
                public Object doInRedis(RedisConnection connection) {
                    connection.publish(OperConfig.topicName.getBytes(), SerializationUtils.serializeNative(event));
                    return null;
                }
            }, true);
        } catch (Exception e) {
            ExceptionUtils.logTrace(logger, e);
        }

    }

    public RestTemplate restTemplate;


    @PostConstruct
    public void init() {
        activeProfile = (System.getProperty("spring.profiles.active"));
        runningNodeOps = Collections.synchronizedList(new ArrayList());
        initRestTemplate();
        initSpace();
    }

    private void initSpace() {
        File file = new File(WORKSPACE);
        if (!file.exists()) {
            file.mkdir();
        }
    }

    private List<NodeOperation> runningNodeOps;

    private void initRestTemplate() {
        RequestConfig config = RequestConfig.custom().setConnectionRequestTimeout(1000).setConnectTimeout(1000)
                .setSocketTimeout(2000).build();
        HttpClientBuilder builder = HttpClientBuilder.create().setDefaultRequestConfig(config)
                .setRetryHandler(new DefaultHttpRequestRetryHandler(1, false));
        CredentialsProvider provider = new BasicCredentialsProvider();
        // Create the authentication scope
        AuthScope scope = new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM);
        // Create credential pair，在此处填写用户名和密码
        UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(adminUser, adminPassword);
        // Inject the credentials
        provider.setCredentials(scope, credentials);
        builder.setDefaultCredentialsProvider(provider);
        HttpClient httpClient = builder.build();
        ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
        restTemplate = new RestTemplate(requestFactory);

    }

    private void fixPath(String prefix, String[] pathSegs) {
        StringBuilder prefixBuf = new StringBuilder(prefix);
        for (String pathSeg : pathSegs) {
            prefixBuf.append(pathSeg).append(File.separator);
            File file = new File(prefixBuf.toString());
            if (!file.exists()) {
                file.mkdir();
            }
        }
    }


    public boolean checkIsActive() {
        try {
            if ("1".equals(this.restTemplate.getForObject("http://localhost:9309/isActive", String.class))) {
                return true;
            }
        } catch (Exception e) {
            logger.error("", e);
        }
        return false;
    }

    public String process(NodeOperation operation) {
        try {
            if (OperationType.start == operation.getType()
                    || OperationType.silentStart == operation.getType() ) {
                if (startService(operation)) {
                    runningNodeOps.add(operation);
                }
            } else if (OperationType.shutdown == operation.getType()) {
                shutdownService(operation, false);
            } else if (OperationType.restart == operation.getType()) {
                if (reStartService(operation)) {
                    runningNodeOps.add(operation);
                }
            } else if (OperationType.pause == operation.getType()) {
                pauseService(operation);
            } else if (OperationType.resume == operation.getType()) {
                resumeService(operation);
            } else if (OperationType.forceShutdown == operation.getType()) {
                shutdownService(operation, true);
            } else {
                throw new UnsupportedOperationException(operation.getType().toString());
            }
        } catch (Exception e) {
            ExceptionUtils.logTrace(logger, e);
            return e.getMessage();
        }

        return null;
    }

    private boolean testActive(NodeOperation operation) {
        StringBuilder urlBuf = new StringBuilder("http://")
                .append(operation.getIp()).append(":")
                .append(operation.getPort())
                .append("/testActive");
        try {
            return "1".equals(restTemplate.getForObject(urlBuf.toString(), String.class));
        } catch (Exception e) {
        }
        return false;

    }

    @Scheduled(fixedDelay = 10000)
    public void processFlow() {
        for (int i = runningNodeOps.size() - 1; i > -1; i--) {
            NodeOperation operation = runningNodeOps.get(i);
            if (operation.getType() == OperationType.start || operation.getType() == OperationType.silentStart) {
                if (testActive(operation)) {
                    runningNodeOps.remove(i);
                    publishEvent(new RouteServerEvent(operation.getIp(), operation.getPort()
                            , operation.getServiceName(), operation.getType() == OperationType.start?
                            InstanceState.active:InstanceState.pause, operation.getBranch()));
                }
            } else if (operation.getType() == OperationType.restart) {
                if (!testActive(operation)) {
                    operation.setType(OperationType.start);
                    //next step
                    startService(operation);
                }
            }
        }

    }

    public boolean pauseService(NodeOperation operation) {
        StringBuilder urlBuf = new StringBuilder(eurekaZone)
                .append("apps/")
                .append(operation.getServiceName())
                .append("/")
                .append(operation.getIp())
                .append(":")
                .append(operation.getPort())
                .append("/metadata?isPaused=1");
        try {
            restTemplate.put(urlBuf.toString(), null);
        } catch (Exception e) {
            return false;
        }
        publishEvent(new RouteServerEvent(operation.getIp()
                , operation.getPort(), operation.getServiceName(), InstanceState.pause, operation.getBranch()));
        return true;
    }

    public boolean resumeService(NodeOperation operation) {
        StringBuilder urlBuf = new StringBuilder(eurekaZone)
                .append("apps/")
                .append(operation.getServiceName())
                .append("/")
                .append(operation.getIp())
                .append(":")
                .append(operation.getPort())
                .append("/metadata?isPaused=0");
        try {
            restTemplate.put(urlBuf.toString(), null);
        } catch (Exception e) {
            return false;
        }
        publishEvent(new RouteServerEvent(operation.getIp()
                , operation.getPort(), operation.getServiceName(), InstanceState.active, operation.getBranch()));
        return true;

    }

    //http://localhost:19300/eureka/apps/CRM-GROUPORDER-SERVICE/192.168.1.212:19900/status?value=OUT_OF_SERVICE

    private boolean startService(NodeOperation operation) {
        StringBuilder commandBuf = new StringBuilder();
        try {
            String userPath = REPO + operation.getPath();
            boolean isWinOs = ShellUtils.isWinOs();
            if (isWinOs) {
                commandBuf.append("start /b ");
            } else {
                commandBuf.append("nohup ");
            }
            String userWorkDir = userPath + operation.getPort();

            commandBuf.append("java ")
                    .append("-Duser.dir=\"")
                    .append(userWorkDir);
            fixPath(userPath, new String[]{String.valueOf(operation.getPort()), "public"});
            userWorkDir += File.separator;
            fixPath(userWorkDir, new String[]{"src", "main", "webapp"});
            commandBuf.append("\" -Dspring.application.name=\"")
                    .append(operation.getServiceName())
                    .append("\" -Dserver.port=\"")
                    .append(operation.getPort());
            if(!StringUtils.isBlank(activeProfile)) {
                commandBuf.append("\" -Dspring.profiles.active=\"")
                        .append(activeProfile);
            }
            String branch = operation.getBranch();
            if (StringUtils.isBlank(branch)) {
                branch = "master";
            }
            if(operation.getType() == OperationType.silentStart) {
                commandBuf.append("\" -Deureka.instance.metadataMap.isPaused=\"1");
            }
            commandBuf.append("\" -Deureka.instance.metadataMap.branch=\"")
                    .append(branch)
                    .append("\" -Xms1024m -Xmx1024m -jar ")
                    .append(userPath + operation.getJarName())
                    .append(" > ").append(userWorkDir)
                    .append("out.log");
            if (!isWinOs) {
                commandBuf.append(" &");
            }
            ShellUtils.invoke(commandBuf.toString());
            return true;
        } catch (Exception e) {
            ExceptionUtils.logTrace(logger, e);
            return false;

        }
    }

    private void killPid(NodeOperation operation) throws Exception{
            boolean isWinOs = ShellUtils.isWinOs();
            String pid = null;
            if(!isWinOs) {
                pid = ShellUtils.invoke(new StringBuilder("jps -v|grep ").append(operation.getJarName())
                        .append("|grep ").append(operation.getPort()).toString());
            } else {
                pid = ShellUtils.invoke(new StringBuilder("jps -v|findStr ").append(operation.getJarName())
                        .append("|findStr ").append(operation.getPort()).toString());
                String findStr = "findStr " + operation.getPort();
                int x = pid.indexOf(" "+operation.getJarName() + " ");
                if(x > -1) {
                    pid = pid.substring(pid.indexOf(findStr)+findStr.length()+1, x);
                } else {
                    pid = null;
                }


            }

            if (!StringUtils.isBlank(pid)) {
                if(!isWinOs) {
                    ShellUtils.invoke("kill -9 " + pid);
                } else {
                    ShellUtils.invoke("tskill " + pid);
                }

            }

    }

    private boolean reStartService(NodeOperation operation) {
        return shutdownService(operation, false);
    }

    public boolean shutdownService(NodeOperation operation, boolean isForce) {
        try {

            publishEvent(new RouteServerEvent(operation.getIp()
                    , operation.getPort(), operation.getServiceName(), InstanceState.shutdown, operation.getBranch()));

            if (!isForce) {

                ResponseEntity entity = restTemplate.postForEntity(new StringBuilder("http://")
                                .append(operation.getIp())
                                .append(":")
                                .append(operation.getPort())
                                .append("/shutdown").toString(), null
                        , String.class);
                if (String.valueOf(entity.getBody()).indexOf("Shutting down, bye...") > -1) {
                    return true;
                }
            } else {
                killPid(operation);
            }


        } catch (Throwable t) {
            ExceptionUtils.logTrace(logger, t);
        }
        return false;
    }
}
